Description:
安裝 nodemon
到這裡決定先安裝 nodemon,透過nodemon 啟動 Server 的話,只要 js 有更動,就會自動重啟
雖然講師重啟得很開心,但我有點懶得一直手動重啟
註:jade & css 除外,改這兩個不需要重啟Server,nodemon 也不會偵測到這個改動
使用 npm install 安裝
加上 --save-dev option 代表這個 dependencies 是給開發人員使用的,也會自動加到package.jsonnpm install --save-dev nodemon
安裝完打開package.json,在 scripts 區塊加上執行 nodemon 指令: "dev"
...
  "scripts": {
    "start": "node ./bin/www",
    "dev": "nodemon ./bin/www"
  },
...
使用以下指定啟動,之後程式變更時就會自動重啟 Servernpm run dev

App & Module Setup
安裝express-generator globallynpm install -g express-generator
透過express建立新project目錄 4_nodeblogexpress 4_nodeblog
修改package.json,加入要用的 dependencies
{
  "name": "4-nodeblog",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www",
    "dev": "nodemon ./bin/www"
  },
  "dependencies": {
    "body-parser": "~1.16.0",
    "cookie-parser": "~1.4.3",
    "debug": "~2.6.0",
    "express": "~4.14.1",
    "jade": "~1.11.0",
    "morgan": "~1.7.0",
    "serve-favicon": "~2.3.2",
    "monk": "https://github.com/vccabral/monk.git",
    "connect-flash": "*",
    "express-session": "*",
    "express-validator": "*",
    "express-messages": "*",
    "multer": "*",
    "moment": "*",
    "mongodb": "*"
  },
  "devDependencies": {
    "nodemon": "^1.11.0"
  }
}
monk: 類似於mongoose,MongoDB ORM,這邊用monk是想提供多種練習,再去選擇自己喜歡哪一種
moment: javascript library,用來format 日期時間格式
其他 module 都和 nodeauth project 類似,就不再多說明
安裝 modulesnpm install
修改 app.js,import module
var session = require('session');
var multer = require('multer');
var upload = multer({ dest: './public/images' })
var expressValidator = require('express-validator');
var mongo = require('mongodb');
var db = require('monk')('localhost/nodeblog');
app.locals.moment = require('moment');
routing,讓 router 可以存取到 DB
// Make our db accessible to our router
app.use(function(req, res, next){
    req.db = db;
    next();
});
加入 connect-flash, validator, session middleware (從 project 3 copy過來)
// Connect-Flash
app.use(require('connect-flash')());
app.use(function (req, res, next) {
  res.locals.messages = require('express-messages')(req, res);
  next();
});
// validator
app.use(expressValidator({
  errorFormatter: function(param, msg, value) {
      var namespace = param.split('.')
      , root    = namespace.shift()
      , formParam = root;
    while(namespace.length) {
      formParam += '[' + namespace.shift() + ']';
    }
    return {
      param : formParam,
      msg   : msg,
      value : value
    };
  }
}));
// Handle Sessions
app.use(session({
    secret:'secret',
    saveUninitialized: true,
    resave: true
}));
Layout template
這次不使用bootstrap,只使用jade
layout.jade
doctype html
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
  body
   .container
    img.logo(src='/images/nodebloglogo.png')
    nav
     ul
      li
       a(href='/') Home
      li
       a(href='/posts/add') Add Post
      li
       a(href='/categories/add') Add Category
    block content
    footer
     p NodeBlog © 2017
這邊會需要一張 logo 圖,可以隨意放自己喜歡的圖,或是到這個免費建logo的網址做一個
把圖片放到 project 下的 public\images 中,檔名需與 layout.jade 中定義的一致 (nodebloglogo.png)
修改style.css,撰寫css樣式
body {
  font: 15px Helvetica, Arial, sans-serif;
  background: #f4f4f4;
  color: #666;
}
.logo {
    text-align: center;
    margin: auto;
    padding-bottom: 10px;
    display: block;
}
.container {
    width: 750px;
    border: 1px solid #ccc;
    margin: 20px auto;
    padding: 20px;
    border-top: #83cd39 3px solid;
}
.clr {
    clear: both;
}
ul {
    padding: 0;
    margin: 0;
}
h1,h2,h3,p {
    padding: 5px 0;
    margin-bottom: 0;
}
p {
    margin: 0;
}
a {
  color: #00B7FF;
}
nav {
    background: #404137;
    overflow: auto;
    height: 40px;
    padding: 20px 0 0 10px;
    font-size: 10px;
}
nav li {
    float: left;
    list-style: none;
}
nav a {
    padding: 10px;
    margin: 0 10px;
    color: #fff;
}
nav a.current, nav a:hover {
    background: #83cd29;
    color: #000;
}
除了圖片之外,其他樣式應該如下圖
上面的配色可以隨意調配,分享兩個之前我自己有在用的網站:
Color Drop 這個網站提供多種顏色並排的比較,可以用來查看網站多種色彩的搭配效果
Paletton 也提供網站色彩搭配,而且附有一個大大的調色盤
首頁顯示貼文
在 Mongo Shell create nodeblog DB,並新增 categories 和 posts 兩個 collection
use nodeblog
db.createCollection('categories');
db.createCollection('posts');
新增兩筆資料,等下測試要用
db.posts.insert({title:"Blog Post One", category:"Technology", arthor:"yuki", body:"This is the bo dy", date:ISODate()});
db.posts.insert({title:"Blog Post Two", category:"Science", arthor:"grace", body:"This is the body ", date:ISODate()});
query posts collection,確認資料有塞進去db.posts.find().pretty();
修改 routes\index.js,加入 mongo db module 及 HTTP GET request
var express = require('express');
var router = express.Router();
//mongo db
var mongo = require('mongodb');
var db = require('monk')('localhost/nodeblog');
/* GET home page. */
router.get('/', function(req, res, next) {
    var db = req.db;
    var posts = db.get('posts');
    posts.find({}, {}, function(err, posts){
        res.render('index', { posts: posts });
    });
});
module.exports = router;
修改 index.jade,如果有任何posts,把每個post列出來
title含有超連結,利用post的 _id 作為routing path
extends layout
block content
  if posts
   each posts, i in posts
    .post
     h1
      a(href='/posts/show/#{post._id}')
       =post.title

修改 style.css
美化幾個地方
/* 分類 */
.meta{
    padding: 7px;
    border: 1px solid #ccc;
    background: #ccc;
    margin-bottom: 10px;
}
/* Read More 連結 */
a.more{
    display: block;
    width: 80px;
    background: #404137;
    color: #fff;
    padding: 10px;
    margin-top: 30px;
    text-decoration: none;
}
/* 貼文 */
.post{
    border-bottom: 1px solid #ccc;
    padding-bottom: 20px;
}
/* 貼文Title連結 */
.post h1 a{
    color: #666;
    text-decoration: none;
}
使用 Moment 加上貼文的分類、作者、日期和貼文內容,最後再加上 Read More 連結
extends layout
block content
  if posts
   each post, i in posts
    .post
     h1
      a(href='/posts/show/#{post._id}')
       =post.title
     p.meta Posted in #{post.category} by #{post.author} on #{moment(post.date).format("MM-DD-YYYY")}
     =post.body
     a.more(href='/posts/show/#{post._id}') Read More
弄完之後應該長得像這樣
新增貼文功能
接下來要撰寫新增貼文的功能
修改 app.js 的 routing,把 users 改成 posts
...
var index = require('./routes/index');
var posts = require('./routes/posts');
...
app.use('/', index);
app.use('/posts', posts);
...
在 routes 下新增 posts.js,內容從 user.js copy過來,把user.js刪掉(這邊用不到)
修改成下面這樣:
var express = require('express');
var router = express.Router();
router.get('/add', function(req, res, next) {
    res.render('addpost', {
        'title': 'Add Post'
    });
});
module.exports = router;
接下來要為 posts 新增 view
在 view 下新增 addpost.jade,撰寫template
extends layout
extends layout
block content
    h1=title
    ul.errors
        if errors
            each error, i in errors
                li.alert.alert-danger #{error.msg}
    form(method='post', action='/posts/add', enctype="multipart/form-data")
        .form-group
            label Title:
            input.form-control(name='title', type='text')
        .form-group
            label Category:
            select.form-control(name='category')           
        .form-group
            label Body:
            textarea.form-control(name='body', id='body')
        .form-group
            label Main Image:
            input.form-control(name='mainimage', type='file')
        .form-group
            label Author:
            select.form-control(name='author')
                option(value='byakuinss') byakuinss
                option(value='yuki') yuki
        input.btn.btn-default(name='submit', type='submit', value='Save')
html 寫完了,再來修改 style.css,為 addpost.jade 加上css樣式
input, select, textarea{
    margin-bottom: 15px;
}
label{
    display: inline-block;
    width: 180px;
}
input[type='text'], select, textarea{
    padding: 3px;
    height: 20px;
    width: 200px;
    border: 1px #ccc solid;
}
select{
    height: 28px;
}
textarea{
    height: 70px;
    width: 400px;
}
接下來要將貼文存到DB,需要在 posts.js 加入 HTTP POST request
POST request 主要有以下幾個動作
//Require multer to handle image
var multer  = require('multer');
var upload = multer({ dest: './public/images' });
     ......
router.post('/add', upload.single('mainimage'), function(req, res, next) {
    //Get Form Values
    var title = req.body.title;
    var category = req.body.category;
    var body = req.body.body;
    var author = req.body.author;
    var date = new Date();
    //Check Image Upload
    if(req.file){
        var mainimage = req.file.filename;
    } else {
        var mainimage = 'no-image.jpg'
    }
    //Form Validation
    req.checkBody('title', 'Title field is required').notEmpty();
    req.checkBody('body', 'Body field is required').notEmpty();
    //Check Errors
    var errors = req.validationErrors();
    if(errors){
        res.render('addpost', {
            "errors": errors
        });
    } else {
        var posts = db.get('posts');
        posts.insert({
            "title": title,
            "body": body,
            "category": category,
            "date": date,
            "author": author,
            "mainimage": mainimage
        }, function(err, post){
            if(err) {
                res.send(err);
            } else {
                req.flash('success', 'Post Added');
                res.location('/');
                res.redirect('/');
            }
        });
    }
});
     ......
重啟 Server,試著新增一篇貼文,此時的 Category 沒有資料,這項先跳過不選
新增完應該會看到剛剛新增的貼文出現在首頁
現在來補足 Category 選單,先到 Mongo Shell在 categories collection 手動新增幾筆資料
db.categories.insert({name:'Technology'});
db.categories.insert({name:'Science'});
db.categories.insert({name:'Business'});
要讓 Category 選單從 categories collection 抓出資料,需要在 GET Add Post 頁面時加入DB連線,並將資料存到 categories
修改 posts.js 的 GET request 內容,將取得的DB資料存入 categories 參數
router.get('/add', function(req, res, next) {
    var categories = db.get('categories');
    categories.find({}, {}, function(err, categories){
        res.render('addpost', {
            'title': 'Add Post',
            'categories': categories
        });       
    });
});
接下來要從 categories 參數中取出值,並串連到 addpost.jade 的 category 選單
修改 addpost.jade,在 Category 選單下加入兩行,針對每一個 category,在選單中顯示
category.name
        ...
        .form-group
            label Category:
            select.form-control(name='category')
                each category, i in categories
                    option(value='#{category.name}') #{category.name}
        ...
在選單中可以看到所有 categories 了
新增一篇帶有 category 的貼文,測試成功
文字編輯器
在新增貼文時,如果有文字編輯器,貼文的內容就可以有更多變化
這邊選擇的是 CKEditor,因為比較容易Setup
到CKEditor官網下載 Standard Package
下載後解壓會產生ckeditor folder,把整個 folder 複製到 project folder 的 public 資料夾下
修改 addpost.jade,在最下方加入script,import ckeditor.js,並且用來取代原本 body 區塊
...
        input.btn.btn-default(name='submit', type='submit', value='Save')
        script(src='/ckeditor/ckeditor.js')
        script
            | CKEDITOR.replace('body');
...
重新進入 Add Post 頁面,就會看到原本 body 區塊的 textarea 已經變成文字編輯器囉
新增Category功能
修改 app.js,加入 categories routing
...
var index = require('./routes/index');
var posts = require('./routes/posts');
var categories = require('./routes/categories')
...
app.use('/', index);
app.use('/posts', posts);
app.use('/categories', categories);
...
新增兩個檔案: addcategory.jade \ categories.js
複製 addpost.jade 到 addcategory.jade,只留下一個 text 和 button,如下
extends layout
block content
    h1=title
    ul.errors
        if errors
            each error, i in errors
                li.alert.alert-danger #{error.msg}
    form(method='post', action='/categories/add')
        .form-group
            label Name:
            input.form-control(name='name', type='text')
        input.btn.btn-default(name='submit', type='submit', value='Save')
同樣複製 posts.js 到 categories.js,只留下需要的 module,修改 GET \ POST request 內容
var express = require('express');
var router = express.Router();
var mongo = require('mongodb');
var db = require('monk')('localhost/nodeblog');
router.get('/add', function(req, res, next) {
    res.render('addcategory', {
        'title': 'Add Category'
    });   
});
router.post('/add', function(req, res, next) {
    //Get Form Values
    var name = req.body.name;
    //Form Validation
    req.checkBody('name', 'Name field is required').notEmpty();
    //Check Errors
    var errors = req.validationErrors();
    if(errors){
        res.render('addcategories', {
            "errors": errors
        });
    } else {
        var categories = db.get('categories');
        categories.insert({
            "name": name
        }, function(err, category){
            if(err) {
                res.send(err);
            } else {
                req.flash('success', 'Category Added');
                res.location('/');
                res.redirect('/');
            }
        });
    }
});
module.exports = router;
新增一個 Category 測試,新增完再到 Add Post,新category已經出現在選單中了

縮短文字內容 (truncate text) & 顯示上傳圖片
把之前測試的新增貼文都刪除db.posts.remove("");
修改layout.jade,加入success message
    ...
      li
       a(href='/categories/add') Add Category
    != messages()
    block content
    ...
修改style.css,加入 success message 顯示的css樣式
...
ul.success li{
    padding: 15px;
    margin-top: 10px;
    margin-bottom: 20px;
    border: 1px solid transparent;
    border-radius: 4px;
    color: #3c763d;
    background-color: #dff0d8;
    border-color: #d6e9c6;
    list-style: none;
}
接下來加入truncate text效果
新增一篇很長的貼文
修改app.js,加入一個新function: truncateText
...
app.locals.moment = require('moment');
app.locals.truncateText = function(text, length){
  var truncateText = text.substring(0, length);
  return truncateText;
}
...
修改index.jade,將=post.body改成 !=truncateText(post.body,400)
  ......
     p.meta Posted in #{post.category} by #{post.author} on #{moment(post.date).format("MM-DD-YYYY")}
     !=truncateText(post.body,400)
     a.more(href='/posts/show/#{post._id}')
  ...... 
重啟server,可以看到過長的貼文被截掉了
接下來要把 image 加入貼文
先確認新增貼文時上傳的圖片有在 public\images中
修改 index.jade,在 p.meta 下方加入圖片label (可以依自己喜好隨意放)
     ...
     p.meta Posted in #{post.category} by #{post.author} on #{moment(post.date).format("MM-DD-YYYY")}
     img(src='/images/#{post.mainimage}')
     !=truncateText(post.body,400)
     ...
修改 style.css加上圖片css樣式,因為上傳的圖片可能大小不一,我希望圖片都是隨視窗改變大小
.post img {
    width: 100%;
}
重新整理網頁,圖片就出來囉
註:沒看到圖片怎麼辦
以Category View檢視貼文
需要新增一個新頁面,用Category當作query條件show出對應的貼文
修改categories.js,加入新的 route,將要query的category條件放入 posts.find({query_condition}, {}, function ...)
router.get('/show/:category', function(req, res, next) {
    var posts = db.get('posts');
    posts.find({category: req.params.category}, {}, function(err, posts){
        res.render('index', {  //切換回index以顯示貼文
            'title': req.params.category, //標題為query的category
            'posts': posts  //顯示query到的貼文
        });       
    });   
});
修改index.jade,將category文字改成連結,點選連結就會顯示出該分類的所有貼文
    ....
     p.meta Posted in
      a(href='/categories/show/#{post.category}') #{post.category}
      by #{post.author}
      on #{moment(post.date).format("MM-DD-YYYY")}
     img(src='/images/#{post.mainimage}')
    ...
重啟 Server 測試,看到 Category 文字帶有連結,且點選後只出現該分類的貼文
檢視單篇貼文內容
目前點選 Read More 按鈕,還無法顯示單篇貼文內容
和分類顯示相同,需要為單篇貼文顯示建立新的 route
修改 posts.js,透過 id 找到對應的貼文並以 show view 顯示
...
router.get('/show/:id', function(req, res, next) {
    var posts = db.get('posts');
    posts.findById(req.params.id, function(err, post){
        res.render('show', {
            'post': post
        });       
    });
});
...
新增 views\show.jade 檔案,內容從 index.jade copy 過來修改
因為show只需要顯示單篇貼文,所以去掉 loop, title 連結,truncate text改成顯示完整body
extends layout
block content
    .post
     h1=post.title
     p.meta Posted in
      a(href='/categories/show/#{post.category}') #{post.category}
      by #{post.author}
      on #{moment(post.date).format("MM-DD-YYYY")}
     img(src='/images/#{post.mainimage}')
     !=post.body
重啟 Server,點選 Read More 就可以顯示完整的單篇貼文了
在單篇貼文中加入 comments 區塊
修改 show.js,從 post.body 往下新增 comments 區塊
如果目前有任何 comments就會顯示,無論是否有已存在 comments 都會顯示新增comment的表單
     ...
     img(src='/images/#{post.mainimage}')
     !=post.body
     br
     hr
     if post.comments
      h3 Comments
      each comment, i in post.comments
       .comment
        p.comment-name #{comment.name}
        p.comment-body #{comment.body}
      br
    h3 Add Comment
    if errors
     ul.errors
      each error, i in errors
       li.alert.alert-danger #{error.msg}
    form.comment-form(method='post', action='/posts/addcomment')
     input(name='postid', type='hidden', value='#{post._id}')
     .form-group
      label Name
      input.form-control(type='text', name='name')
     .form-group
      label Email
      input.form-control(type='text', name='email')     
     .form-group
      label Body
      textarea.form-control(type='text', name='body')
     br
     input.btn.btn-default(type='submit', name='submit', value='Add Comment')

接下來為 Add Comment 按鈕撰寫 POST request
修改 post.js,加入新的 POST route (從 add post copy)
router.post('/addcomment', function(req, res, next) {  //修改名稱為addcomment
    //Get Form Values
    var name = req.body.name;
    var email = req.body.email;
    var body = req.body.body;
    var postid = req.body.postid;
    var commentdate = new Date();
    //Form Validation
    req.checkBody('name', 'Name field is required').notEmpty();
    req.checkBody('email', 'Email field is required but never displayed').notEmpty();
    req.checkBody('email', 'Email field is not formatted properly').isEmail();       
    req.checkBody('body', 'Body field is required').notEmpty();
    //Check Errors
    var errors = req.validationErrors();
    if(errors){  //如果add comment有error仍要顯示貼文
        var posts = db.get('posts');  
        posts.findById(postid, function(err, post){
            res.render('show', {
                "errors": errors,
                "post": post
            });           
        });
    } else { //如果沒有錯誤,將comment內容update到該篇貼文的comments欄位
        var comment = {
            "name": name,
            "email": email,
            "body": body,
            "commentdate": commentdate
        }
        var posts = db.get('posts');
        
        posts.update({
            "_id": postid
        }, {
            $push: {
                "comments": comment
            }
        }, function(err, doc){  //都沒有問題就切換到單篇貼文頁面
            if(err){
                throw err;
            } else {
                req.flash('success', 'Comment Added');
                res.location('/posts/show/'+postid);
                res.redirect('/posts/show/'+postid);
            }
        });
    }
});
重啟 Server,新增一篇 comment 測試看看,網頁會自動切換,也會顯示成功新增comment的訊息
